Author: Jorge Maldonado Ventura
Category: Python
Date: 2016-09-16 17:40
Lang: es
Modified: 2017-02-25 20:58
Slug: creacion-de-un-videojuego-con-pygame
Status: published
Tags: lenguage de programación, pygame, Python, Python 3, software libre, videojuego, videojuegos
Title: Creación de un videojuego con pygame

En esta entrada, vamos a ver cómo crear un videojuego con Python y
pygame. El videojuego se llama *Bullet dodger.* El objetivo es esquivar
todas las balas que se disparen para conseguir el mayor número de puntos
posibles. Cada bala disparada aumenta la puntuación en 1. El personaje
se maneja con el ratón, y el juego tiene un modo de pantalla completa.

Antes de comenzar a programar, debes asegurarte de que tienes todos los
materiales necesarios. Solamente necesitas [pygame](http://pygame.org/),
[Python](https://www.python.org/) y un editor de texto o IDE con el que
te sientas cómodo. Abajo tienes la bala que utilizaremos para el juego;
descárgala y ubícala en la carpeta donde vayas a programar.
<a href="/wp-content/uploads/2016/09/bullet.png"><img class="aligncenter size-full wp-image-349" src="/wp-content/uploads/2016/09/bullet.png" alt="bullet" width="10" height="13" /></a>

### Paso 1: Crear ventana básica

Lo primero que hay que hacer para utilizar pygame, es importarlo.
Normalmente son necesarias las librerías `pygame` y `pygame.locals`.
Crea un archivo de llamado `main.py` e importa dichas librerías.

<!-- more -->

    :::python3
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    from pygame.locals import *
    import pygame

Una vez importado todo lo necesario, podemos crear el videojuego. Lo
primero es hacer la ventana.

    :::python3
    pygame.init()
    # Display
    screen = pygame.display.set_mode((800, 600))
    # Window titlebar
    pygame.display.set_caption('Bullet dodger')
    pygame.display.set_icon(pygame.image.load('bullet.png'))

Hemos iniciado `pygame`. Después, hemos establecido la resolución a
800x600 píxeles y, por último, hemos creado el título de nuestra ventana
y le hemos asignado como icono la imagen de la bala que hemos
descargado. Cuando lo ejecutemos, va a aparecer una ventana negra que se
cerrará inmediatamente, porque cuando termina la última instrucción el
programa ya no tiene nada más que hacer y finaliza.

Para evitar esto necesitamos que el programa se ejecute hasta que el
usuario decida cerrarlo. Para ello vamos a crear el bucle del juego.
Dentro de ese bucle, vamos a comprobar si el usuario ha realizado alguna
acción. Si la acción realizada es la de cerrar la ventana
(`event.type == QUIT`), pygame parará y la variable `running` tendrá el
valor `False`; por lo que se saldrá del bucle y se finalizará el
programa.

    :::python3
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == QUIT:
                running = False
    pygame.quit()

<a href="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-032822.png"><img class="aligncenter size-full wp-image-390" src="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-032822.png" alt="Ventana simple en pygame" width="838" height="708" srcset="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-032822.png 838w, /wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-032822-300x253.png 300w, /wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-032822-768x649.png 768w" sizes="(max-width: 838px) 100vw, 838px"></a>

A continuación os dejo el código fuente completo.

    :::python3
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    from pygame.locals import *
    import pygame

    pygame.init()
    # Display
    screen = pygame.display.set_mode((800, 600))
    # Window titlebar
    pygame.display.set_caption('Bullet dodger')
    pygame.display.set_icon(pygame.image.load('bullet.png'))
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == QUIT:
                running = False
    pygame.quit()

### Paso 2: Crear la pantalla de inicio

Vamos a crear un archivo en el que ubicaremos algunas constantes
importantes en pygame: los colores y la resolución de la ventana. El
archivo se llamará `global_constants.py`.

    :::python3
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-

    # Resolution
    WIDTH = 800
    HEIGHT = 600

    # Colors
    BLACK = (0, 0, 0)
    GREEN = (0, 255, 0)
    RED = (255, 0, 0)
    YELLOW = (255, 255, 12)

La anchura (`WIDTH`) será de 800 píxeles; la altura (`HEIGHT`), de 600.
Los colores en pygame son una tupla de tres valores: rojo, verde y
amarillo (de ahí las siglas
<abbr title="Red Green Blue">RGB</abbr>). Vamos a crear los
colores negro, verde, rojo y amarillo.

Ahora que hemos creado unas variables para la resolución, podemos
utilizarlas en `main.py`. Vamos a añadir también un tipo de fuente para
escribir texto normal (un poco más adelante veremos cómo funciona el
constructor `pygame.font.Font`).

    :::python3 hl_lines="6 10 16"
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    from pygame.locals import *
    import pygame

    from global_constants import *

    pygame.init()
    # Display
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    fullscreen = False
    # Window titlebar
    pygame.display.set_caption('Bullet dodger')
    pygame.display.set_icon(pygame.image.load('bullet.png'))

    default_font = pygame.font.Font(None, 28)

Ahora vamos a crear una función para escribir texto en pygame.

    :::python3
    def draw_text(text, font, surface, x, y, main_color, background_color=None):
        textobj = font.render(text, True, main_color, background_color)
        textrect = textobj.get_rect()
        textrect.centerx = x
        textrect.centery = y
        surface.blit(textobj, textrect)

La función la hemos llamado `draw_text`. Tiene como parámetros el texto
que queramos escribir, el tipo de fuente, la superficie donde queremos
que se escriba el texto, las coordenadas (`x` y `y`), el color de las
letras y el color de fondo tras las letras (`background_color=None`,
opcional).

Dentro de la función hemos creado el objeto de texto, con la función
`font.render` utilizando los parámetros. Con el objeto de texto, hemos
accedido a las coordenadas rectangulares y las hemos modificado con los
valores de `y` y `x`. Una vez hecho esto, podemos dibujar el texto en la
superficie (`surface.blit(textobj, textrect)`).

Ahora crearemos la función de la pantalla de inicio del juego en
`main.py`.

    :::python3
    def start_screen():
        pygame.mouse.set_cursor(*pygame.cursors.diamond)
        while True:
            title_font = pygame.font.Font('freesansbold.ttf', 65)
            big_font = pygame.font.Font(None, 36)
            draw_text('BULLET DODGER', title_font, screen,
                      WIDTH / 2, HEIGHT / 3, RED, YELLOW)
            draw_text('Use the mouse to dodge the bullets', big_font, screen,
                      WIDTH / 2, HEIGHT / 2, GREEN, BLACK)
            draw_text('Press any mouse button or S when you\'re ready',
                      default_font, screen, WIDTH / 2, HEIGHT / 1.7, GREEN, BLACK)
            draw_text('Press F11 to toggle full screen', default_font, screen,
                      WIDTH / 2, HEIGHT / 1.1, GREEN, BLACK)
            pygame.display.update()
            for event in pygame.event.get():
                if event.type == pygame.MOUSEBUTTONDOWN:
                    main_loop()
                    return
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_s:
                        main_loop()
                        return
                if event.type == QUIT:
                    return

Con `pygame.mouse.set_cursor(*pygame.cursors.diamond)`, hacemos que el
ratón tenga forma de diamante cuando pasas el ratón por la ventana del
juego. Después creamos otros dos tipos de fuentes, con diferentes
tamaños. El tipo de fuente es `freesansbold.ttf`, que es la fuente
predeterminada de pygame. Pasar `None` como primer parámetro del
constructor `Font` tiene es equivalente a pasar `'freesansbold.ttf'`; el
segundo parámetro hace referencia al tamaño de la fuente. La función
`draw_text` que creamos anteriormente permite crear varios textos
fácilmente. Para que se muestren los textos, debemos ejecutar
`pygame.display.update()`. Después creamos un bucle para capturar los
eventos. Si el usuario pulsa cualquier botón del ratón o la tecla S se
ejecutará la función `main_loop()`, que aún no hemos creado. Si el
usuario cierra la ventana, Python saldrá de la función.

Creemos la función `main_loop`. Simplemente mete el bucle del juego que
creamos anteriormente dentro de la función tal que quede como abajo.

    :::python3
    def main_loop():
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == QUIT:
                    running = False

Ahora solo añade al debajo de todas las funciones creadas una llamada a
`start_screen()` y otra llamada a `pygame.quit()` (sirve para cerrar el
motor de pygame). El archivo `main.py` debe haberte quedado como lo
muestro continuación.

    :::python3
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    from pygame.locals import *
    import pygame

    from global_constants import *

    pygame.init()
    # Display
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    # Window titlebar
    pygame.display.set_caption('Bullet dodger')
    pygame.display.set_icon(pygame.image.load('bullet.png'))

    default_font = pygame.font.Font(None, 28)

    def draw_text(text, font, surface, x, y, main_color, background_color=None):
        textobj = font.render(text, True, main_color, background_color)
        textrect = textobj.get_rect()
        textrect.centerx = x
        textrect.centery = y
        surface.blit(textobj, textrect)


    def start_screen():
        pygame.mouse.set_cursor(*pygame.cursors.diamond)
        while True:
            title_font = pygame.font.Font('freesansbold.ttf', 65)
            big_font = pygame.font.Font(None, 36)
            draw_text('BULLET DODGER', title_font, screen,
                      WIDTH / 2, HEIGHT / 3, RED, YELLOW)
            draw_text('Use the mouse to dodge the bullets', big_font, screen,
                      WIDTH / 2, HEIGHT / 2, GREEN, BLACK)
            draw_text('Press any mouse button or S when you\'re ready',
                      default_font, screen, WIDTH / 2, HEIGHT / 1.7, GREEN, BLACK)
            draw_text('Press F11 to toggle full screen', default_font, screen,
                      WIDTH / 2, HEIGHT / 1.1, GREEN, BLACK)
            pygame.display.update()
            for event in pygame.event.get():
                if event.type == pygame.MOUSEBUTTONDOWN:
                    main_loop()
                    return
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_s:
                        main_loop()
                        return
                if event.type == QUIT:
                    return


    def main_loop():
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == QUIT:
                    running = False

    start_screen()
    pygame.quit()

<a href="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-033008.png"><img class="aligncenter size-full wp-image-391" src="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-033008.png" alt="captura-de-pantalla-de-2016-09-11-033008" width="808" height="626" srcset="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-033008.png 808w, /wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-033008-300x232.png 300w, /wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-033008-768x595.png 768w" sizes="(max-width: 808px) 100vw, 808px" /></a>

### Paso 3: Crear el modo de pantalla completa

En el menú principal del juego vamos a permitir cambiar a un modo de
pantalla completa pulsando F11. Para ello tenemos que crear la función
`toggle_fullscreen()`. Antes vamos a crear una variable llamada
fullscreen y vamos a darle el valor `False`.

    :::python3 hl_lines="3"
    # Display
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    fullscreen = False

Creemos ya la función `toggle_fullscreen()`.

    :::python3
    def toggle_fullscreen():
        if pygame.display.get_driver() == 'x11':
            pygame.display.toggle_fullscreen()
        else:
            global screen, fullscreen
            screen_copy = screen.copy()
            if fullscreen:
                screen = pygame.display.set_mode((WIDTH, HEIGHT))
            else:
                screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
            fullscreen = not fullscreen
            screen.blit(screen_copy, (0, 0))

Si el ordenador del usuario tiene el controlador de vídeo x11, ejecutará
la función `pygame.display.toggle_fullscreen()`. Si no dispone de ese
driver la anterior función no fuencionará, así que utilizamos otro
método. En este, tenemos que hacer globales las variables screen y
fullscreen para poder cambiar sus valores. Hacemos una copia de la
variable screen, si el valor de la variable fullscreen es False,
cambiaremos a pantalla completa con screen =
pygame.display.set\_mode((WIDTH, HEIGHT), pygame.FULLSCREEN); en caso
contrario, volveremos al modo normal. Cambiamos el valor de la variable
fullscreen (si es `True` pasará a `False`; si es `False`, a `True`). La
copia de la variable `screen`, debemos dibujarla en screen, ya que al
cambiar el modo de muestra (`display.set_mode`) se perderán los valores
antiguos.

Dememos permitir llamar a esta función pulsando la tecla F11 en la
función `start_screen()`.

    :::python3 hl_lines="9 10"
    for event in pygame.event.get():
        if event.type == pygame.MOUSEBUTTONDOWN:
            main_loop()
            return
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_s:
                main_loop()
                return
            if event.key == K_F11:
                toggle_fullscreen()
        if event.type == QUIT:
            return

### Paso 4: Crear las balas

Crea un archivo llamado `bullet.py`.

    :::python3
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import random

    from pygame.locals import *
    import pygame

    from global_constants import WIDTH, HEIGHT


    def random_bullet(speed):
        random_or = random.randint(1, 4)
        if random_or == 1:  # Up -> Down
            return Bullet(random.randint(0, WIDTH), 0, 0, speed)
        elif random_or == 2:  # Right -> Left
            return Bullet(WIDTH, random.randint(0, HEIGHT), -speed, 0)
        elif random_or == 3:  # Down -> Up
            return Bullet(random.randint(0, WIDTH), HEIGHT, 0, -speed)
        elif random_or == 4:  # Left -> Right
            return Bullet(0, random.randint(0, HEIGHT), speed, 0)


    class Bullet(pygame.sprite.Sprite):

        def __init__(self, xpos, ypos, hspeed, vspeed):
            super(Bullet, self).__init__()
            self.image = pygame.image.load('bullet.png')
            self.rect = self.image.get_rect()
            self.rect.x = xpos
            self.rect.y = ypos
            self.hspeed = hspeed
            self.vspeed = vspeed

            self.set_direction()

        def update(self):
            self.rect.x += self.hspeed
            self.rect.y += self.vspeed
            if self.collide():
                self.kill()

        def collide(self):
            if self.rect.right < 0 or self.rect.x > WIDTH:
                return True
            elif self.rect.right < 0 or self.rect.y > HEIGHT:
                return True

        def set_direction(self):
            if self.hspeed > 0:
                self.image = pygame.transform.rotate(self.image, 270)
            elif self.hspeed < 0:
                self.image = pygame.transform.rotate(self.image, 90)
            elif self.vspeed > 0:
                self.image = pygame.transform.rotate(self.image, 180)

Este es el código que necesitamos para crear balas. La función
`random_bullet.py` sirve para crear una bala con una dirección aleatoria
(hay cuatro: de arriba abajo, de derecha a izquierda, de abajo arriba y
de izquierda a derecha). Podemos controlar la velocidad a través del
parámetro `speed`. En esta función utilizamos las variables `WIDTH` y
`HEIGHT` para que las balas se creen justo en el borde fuera de la
ventana del juego.

Más abajo creamos la clase Bullet, que hereda de `pygame.sprite.Sprite`.
En el constructor (`__init__`) llamamos al constructor de la clase
padre, establecemos la imagen de la bala, las coordenadas rectangulares
y la velocidad horizontal (`hspeed`) y vertical (`vspeed`). Finalmente
llamamos a la función `set_direction()`.

La función `set_direction()`, simplemente gira la imagen para que la
imagen de la bala no de una apariencia de movimiento equivocada. Es
decir, si la bala se mueve hacia abajo, se gira la imagen 180º, puesto
que la imagen de la bala está mirando hacia arriba.

La función `collide()`, devuelve True si la bala ha salido de la
pantalla.

La función update() simplemente actualiza la posición de la bala y la
destruye si ha salido de la pantalla.

Vamos a crear en el archivo `main.py` las balas para comprobar que
funciona el código escrito. Pero antes tenemos que controlar los
fotogramas por segundos, sino el juego correrá a una velocidad diferente
dependiendo de la potencia del ordenador que lo arranque. Escribe en la
parte superior del archivo

    :::python3 hl_lines="3 4 5"
    pygame.display.set_caption('Bullet dodger')
    pygame.display.set_icon(pygame.image.load('bullet.png'))
    # Timing
    fps_clock = pygame.time.Clock()
    FPS = 60

    default_font = pygame.font.Font(None, 28)

En el bucle del juego, escribe lo siguiente.

    :::python3 hl_lines="2 4 6 7 8 9 10 11 12 13 14 15"
    def main_loop():
        bullets = pygame.sprite.Group()
        running = True
        points = 0
        while running:
            pygame.display.update()
            fps_clock.tick(FPS)
            screen.fill(BLACK)

            if random.randint(1, 5) == 1:
                bullets.add(random_bullet(random.randint(1, 1)))
                points += 1
            draw_text('{} points'.format(points), default_font, screen,
                WIDTH / 2, 20, GREEN)
            bullets.update()
            bullets.draw(screen)

Primero, hemos creado un grupo de sprites llamado bullets para organizar
más fácilmente las balas. Hemos creado una variable llamada `points` que
vale 0 al inicio de la partida. Actualizamos la pantalla con
`pygame.display.update()`. Ejecutamos la función `tick()` para que el
juego no pase de los
<abbr title="fotogramas por segundo">FPS</abbr> que hemos
especificado antes. Después llenamos la pantalla de negro (si no hacemos
esto las balas se dibujarán y no se borrarán de su posición antigua).
Para que no salgan demasiadas balas de golpe creamos una condición, con
`if random.randint(1, 5) == 1` hay un 20% de posibilidades de que se
dispare una bala y aumente la puntuación en 1 en cada iteración. Fuera
de ese `if`, dibujamos la puntuación actual, actualizamos las balas y
las dibujamos en la pantalla.

Para que el código funcione deberás importar el módulo random y todo lo
del archivo `bullet.py` (`from bullet import *`). Así deberá quedar el
código del archivo `main.py`

    :::python3
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import random

    from pygame.locals import *
    import pygame

    from bullet import *
    from global_constants import *

    pygame.init()
    # Display
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    fullscreen = False
    # Window titlebar
    pygame.display.set_caption('Bullet dodger')
    pygame.display.set_icon(pygame.image.load('bullet.png'))
    # Timing
    fps_clock = pygame.time.Clock()
    FPS = 60

    default_font = pygame.font.Font(None, 28)


    def draw_text(text, font, surface, x, y, main_color, background_color=None):
        textobj = font.render(text, True, main_color, background_color)
        textrect = textobj.get_rect()
        textrect.centerx = x
        textrect.centery = y
        surface.blit(textobj, textrect)


    def toggle_fullscreen():
        if pygame.display.get_driver() == 'x11':
            pygame.display.toggle_fullscreen()
        else:
            global screen, fullscreen
            screen_copy = screen.copy()
            if fullscreen:
                screen = pygame.display.set_mode((WIDTH, HEIGHT))
            else:
                screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
            fullscreen = not fullscreen
            screen.blit(screen_copy, (0, 0))


    def start_screen():
        pygame.mouse.set_cursor(*pygame.cursors.diamond)
        while True:
            title_font = pygame.font.Font('freesansbold.ttf', 65)
            big_font = pygame.font.Font(None, 36)
            default_font = pygame.font.Font(None, 28)
            draw_text('BULLET DODGER', title_font, screen,
                      WIDTH / 2, HEIGHT / 3, RED, YELLOW)
            draw_text('Use the mouse to dodge the bullets', big_font, screen,
                      WIDTH / 2, HEIGHT / 2, GREEN, BLACK)
            draw_text('Press any mouse button or S when you\'re ready',
                      default_font, screen, WIDTH / 2, HEIGHT / 1.7, GREEN, BLACK)
            draw_text('Press F11 to toggle full screen', default_font, screen,
                      WIDTH / 2, HEIGHT / 1.1, GREEN, BLACK)
            pygame.display.update()
            for event in pygame.event.get():
                if event.type == pygame.MOUSEBUTTONDOWN:
                    main_loop()
                    return
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_s:
                        main_loop()
                        return
                    if event.key == K_F11:
                        toggle_fullscreen()
                if event.type == QUIT:
                    return


    def main_loop():
        bullets = pygame.sprite.Group()
        running = True
        points = 0
        while running:
            pygame.display.update()
            fps_clock.tick(FPS)
            screen.fill(BLACK)

            if random.randint(1, 5) == 1:
                bullets.add(random_bullet(random.randint(1, 1)))
                points += 1
            draw_text('{} points'.format(points), default_font, screen,
                WIDTH / 2, 20, GREEN)
            bullets.update()
            bullets.draw(screen)

            for event in pygame.event.get():
                if event.type == QUIT:
                    running = False

    start_screen()
    pygame.quit()

<a href="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-052228.png"><img class="aligncenter size-full wp-image-400" src="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-052228.png" alt="captura-de-pantalla-de-2016-09-11-052228" width="808" height="626" srcset="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-052228.png 808w, /wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-052228-300x232.png 300w, /wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-052228-768x595.png 768w" sizes="(max-width: 808px) 100vw, 808px" /></a>

### Paso 5: Crear el personaje

Crea un archivo llamado `block.py` con el código que aparece a
continuación.

    :::python3
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import pygame
    from pygame.locals import *

    from global_constants import YELLOW


    class Block(pygame.sprite.Sprite):
        def __init__(self):
            super(Block, self).__init__()
            self.img = pygame.Surface((30, 30))
            self.img.fill(YELLOW)
            self.rect = self.img.get_rect()
            self.centerx = self.rect.centerx
            self.centery = self.rect.centery

        def set_pos(self, x, y):
            'Positions the block center in x and y location'
            self.rect.x = x - self.centerx
            self.rect.y = y - self.centery

        def collide(self, sprites):
            for sprite in sprites:
                if pygame.sprite.collide_rect(self, sprite):
                    return True

Para crear el personaje no nos hemos complicado mucho: simplemente hemos
creado un cuadrado amarillo.

Aparte de importar lo típico en pygame (líneas 3 y 4), importamos de
global\_constants.py la variable YELLOW para el color del cuadrado.
Hemos creado una clase llamada `Block`, que como `Bullet`, hereda de
`pygame.sprite.Sprite`. En el constructor, ejecutamos el constructor de
la clase padre, creamos una superficie de 30x30 píxeles, la coloreamos
de amarillo y obtenemos y guardamos el centro de la coordenadas del
cuadrado. Nos interesa el centro, porque cuando creemos movamos el
cuadrado con el ratón, el ratón siempre estará en el centro del cuadrado
para hacer que el movimiento sea natural (si no, nos moveríamos a partir
de la esquina superior izquierda del cuadrado).

El método `set_pos`, nos permitirá ubicar el cuadrado a partir de las
coordenadas que le pasemos como parámetro. El centro del cuadrado estará
en las coordenadas `x` y `y`.

El método `collide` devuelve True si el cuadrado colisiona con uno de
los sprites del grupo de sprites pasado como parámetro. La colisión se
calcula a partir de las coordenadas rectangulares
(`pygame.sprite.collide_rect(self, sprite)`), así que se detecta una
colisión cuando se produce una intersección entre los cuadrados de las
imagenes sin importar que una parte sea transparente.

En `main.py`, vamos a importar ahora lo que hemos creado en `block.py`.

    :::python3 hl_lines="2"
    from block import *
    from bullet import *
    from global_constants import *

En el archivo `main.py` ya podemos crear el cuadrado y lo ubicarlo
inicialmente a partir de las coordenadas del ratón. También vamos a
hacer que el ratón sea invisible durante la partida y vamos a crear la
variable `game_over` con el valor `False` (nos servirá más adelante para
detectar el fin del juego).

    :::python3 hl_lines="2 4 5 8"
    def main_loop():
        pygame.mouse.set_visible(False)

        square = Block()
        square.set_pos(*pygame.mouse.get_pos())
        bullets = pygame.sprite.Group()
        running = True
        game_over = False

Después comprobamos si ha colisionado con alguna bala, si lo ha hecho
hacemos que la variable `game_over` sea `True`. Dibujamos el cuadrado
con `screen.blit(square.img, square.rect)` y actualizamos la posición
del ratón cada vez que se mueve el ratón dentro del bucle for que
recorre los eventos de pygame.

    :::python3 hl_lines="1 2 4 6 7 8"
        if square.collide(bullets):
            game_over = True

        screen.blit(square.img, square.rect)
        for event in pygame.event.get():
            if event.type == pygame.MOUSEMOTION:
                mouse_pos = pygame.mouse.get_pos()
                square.set_pos(*mouse_pos)
            if event.type == QUIT:
                running = False

Cuando hayamos terminado, podremos mover el cuadrado amarillo, pero no
sabremos si ha colisionado. Si quieres probar si funciona la detección
de colisiones antes de seguir, muestra algo por pantalla si se cumple la
condición
`if square.collide(bullets):`.

<a href="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-085128.png"><img class="aligncenter size-full wp-image-415" src="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-085128.png" alt="captura-de-pantalla-de-2016-09-11-085128" width="808" height="626" srcset="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-085128.png 808w, /wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-085128-300x232.png 300w, /wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-085128-768x595.png 768w" sizes="(max-width: 808px) 100vw, 808px" /></a>

### Paso 6: Crear la pantalla de fin del juego

Dentro del bucle del juego, detrás del bucle for donde detectamos antes
los movimientos del ratón, vamos a añadir un bucle que se va a ejecutar
mientras la variable `game_over` sea `True`.

    :::python3
    while game_over:
        pygame.mouse.set_visible(True)
        # Text
        draw_text('{}  points'.format(points), default_font, screen,
                  WIDTH / 2, 20, GREEN)
        # Transparent surface
        transp_surf = pygame.Surface((WIDTH, HEIGHT))
        transp_surf.set_alpha(200)
        screen.blit(transp_surf, transp_surf.get_rect())

        draw_text('You lose', pygame.font.Font(None, 40), screen,
                  WIDTH / 2, HEIGHT / 3, RED)
        draw_text('To play again press C or any mouse button',
                  default_font, screen, WIDTH / 2, HEIGHT / 2.1, GREEN)
        draw_text('To quit the game press Q', default_font, screen,
                  WIDTH / 2, HEIGHT / 1.9, GREEN)
        draw_text('Press F11 to toggle full screen', default_font, screen,
                  WIDTH / 2, HEIGHT / 1.1, GREEN, BLACK)

        pygame.display.update()

        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.key == K_F11:
                    toggle_fullscreen()
                if event.key == pygame.K_q:
                    game_over = False
                    running = False
                elif event.key == pygame.K_c:
                    game_over = False
                    main_loop()
                    return  # Avoids recursion
            if event.type == pygame.MOUSEBUTTONDOWN:
                game_over = False
                main_loop()
                return
            if event.type == QUIT:
                game_over = False
                running = False

Dentro del bucle hemos hecho visible de nuevo el ratón y hemos añadido
textos que le indican al usuario lo que puede hacer en este menú. El
texto de la puntuación lo hemos puesto tras una superficie transparente
(`transp_surf`). Para que lo que hemos dibujado sea visible ejecutamos
el método `pygame.display.update()`.

Después, hemos creado un bucle for para comprobar las acciones del
usuario. Si el usuario pulsa F11, se cambiará el juego a modo de
pantalla completa; si pulsa la tecla Q o cierra la ventana, la variable
game\_over será False, por lo que se saldrá de este bucle y del juego;
si pulsa la tecla C o pulsa un botón del ratón, el usuario podrá jugar
otra partida.

<a href="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-093528.png"><img class="aligncenter size-full wp-image-420" src="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-093528.png" alt="captura-de-pantalla-de-2016-09-11-093528" width="808" height="626" srcset="/wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-093528.png 808w, /wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-093528-300x232.png 300w, /wp-content/uploads/2016/09/Captura-de-pantalla-de-2016-09-11-093528-768x595.png 768w" sizes="(max-width: 808px) 100vw, 808px"></a>

### Paso 7: Aumentar la dificultad a medida que se consiguen más puntos

Escribid lo siguiente en el archivo `main.py`

    :::python3 hl_lines="2 3 4 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46"
    points = 0
    min_bullet_speed = 1
    max_bullet_speed = 1
    bullets_per_gust = 1

        while running:
            pygame.display.update()
            fps_clock.tick(FPS)
            screen.fill(BLACK)

            if points >= 2000:
                bullets_per_gust = 3000
                max_bullet_speed = 80
            elif points >= 1000:
                bullets_per_gust = 3
                min_bullet_speed = 3
                max_bullet_speed = 15
            elif points >= 800:
                max_bullet_speed = 20
            elif points >= 600:
                bullets_per_gust = 2
                max_bullet_speed = 10
            elif points >= 500:
                min_bullet_speed = 2
            elif points >= 400:
                max_bullet_speed = 8
            elif points >= 200:
                # The smaller this number is, the probability for a bullet
                # to be shot is higher
                odds = 8
                max_bullet_speed = 5
            elif points >= 100:
                odds = 9
                max_bullet_speed = 4
            elif points >= 60:
                odds = 10
                max_bullet_speed = 3
            elif points >= 30:
                odds = 11
                max_bullet_speed = 2
            elif points < 30:
                odds = 12

            if random.randint(1, odds) == 1:
                for _ in range(0, bullets_per_gust):
                    bullets.add(random_bullet(random.randint(min_bullet_speed,
                                                             max_bullet_speed)))
                    points += 1
            draw_text('{}  points'.format(points), default_font, screen,
                      WIDTH / 2, 20, GREEN)

Las dificultad la controlamos con el número de balas por ráfaga
(`bullets_per_gust`), la velocidad máxima (`max_bullet_speed`) y mínima
(`min_bullet_speed`) de las balas y las probabilidades (`odds` de que se
dispare una ráfaga en cada iteración del bucle del juego. Dependiendo de
la puntuación, estás variables toman un valor distinto. Cuantos más
puntos tienes, más difícil se vuelve el juego.

A continuación os dejo el código definitivo del archivo `main.py`.

    :::python3
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import random

    import pygame
    from pygame.locals import *

    from block import *
    from bullet import *
    from global_constants import *

    pygame.init()
    # Display
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    fullscreen = False
    # Window titlebar
    pygame.display.set_caption('Bullet dodger')
    pygame.display.set_icon(pygame.image.load('bullet.png'))
    # Timing
    fps_clock = pygame.time.Clock()
    FPS = 60

    default_font = pygame.font.Font(None, 28)


    def draw_text(text, font, surface, x, y, main_color, background_color=None):
        textobj = font.render(text, True, main_color, background_color)
        textrect = textobj.get_rect()
        textrect.centerx = x
        textrect.centery = y
        surface.blit(textobj, textrect)


    def toggle_fullscreen():
        if pygame.display.get_driver() == 'x11':
            pygame.display.toggle_fullscreen()
        else:
            global screen, fullscreen
            screen_copy = screen.copy()
            if fullscreen:
                screen = pygame.display.set_mode((WIDTH, HEIGHT))
            else:
                screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
            fullscreen = not fullscreen
            screen.blit(screen_copy, (0, 0))


    def start_screen():
        pygame.mouse.set_cursor(*pygame.cursors.diamond)
        while True:
            title_font = pygame.font.Font('freesansbold.ttf', 65)
            big_font = pygame.font.Font(None, 36)
            draw_text('BULLET DODGER', title_font, screen,
                      WIDTH / 2, HEIGHT / 3, RED, YELLOW)
            draw_text('Use the mouse to dodge the bullets', big_font, screen,
                      WIDTH / 2, HEIGHT / 2, GREEN, BLACK)
            draw_text('Press any mouse button or S when you\'re ready',
                      default_font, screen, WIDTH / 2, HEIGHT / 1.7, GREEN, BLACK)
            draw_text('Press F11 to toggle full screen', default_font, screen,
                      WIDTH / 2, HEIGHT / 1.1, GREEN, BLACK)
            pygame.display.update()
            for event in pygame.event.get():
                if event.type == pygame.MOUSEBUTTONDOWN:
                    main_loop()
                    return
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_s:
                        main_loop()
                        return
                    if event.key == K_F11:
                        toggle_fullscreen()
                if event.type == QUIT:
                    return


    def main_loop():
        pygame.mouse.set_visible(False)

        square = Block()
        square.set_pos(*pygame.mouse.get_pos())
        bullets = pygame.sprite.Group()
        running = True
        game_over = False
        points = 0
        min_bullet_speed = 1
        max_bullet_speed = 1
        bullets_per_gust = 1

        while running:
            pygame.display.update()
            fps_clock.tick(FPS)
            screen.fill(BLACK)

            if points >= 2000:
                bullets_per_gust = 3000
                max_bullet_speed = 80
            elif points >= 1000:
                bullets_per_gust = 3
                min_bullet_speed = 3
                max_bullet_speed = 15
            elif points >= 800:
                max_bullet_speed = 20
            elif points >= 600:
                bullets_per_gust = 2
                max_bullet_speed = 10
            elif points >= 500:
                min_bullet_speed = 2
            elif points >= 400:
                max_bullet_speed = 8
            elif points >= 200:
                # The smaller this number is, the probability for a bullet
                # to be shot is higher
                odds = 8
                max_bullet_speed = 5
            elif points >= 100:
                odds = 9
                max_bullet_speed = 4
            elif points >= 60:
                odds = 10
                max_bullet_speed = 3
            elif points >= 30:
                odds = 11
                max_bullet_speed = 2
            elif points < 30:
                odds = 12

            if random.randint(1, odds) == 1:
                for _ in range(0, bullets_per_gust):
                    bullets.add(random_bullet(random.randint(min_bullet_speed,
                                                             max_bullet_speed)))
                    points += 1
            draw_text('{}  points'.format(points), default_font, screen,
                      WIDTH / 2, 20, GREEN)
            bullets.update()
            bullets.draw(screen)

            if square.collide(bullets):
                game_over = True

            screen.blit(square.img, square.rect)
            for event in pygame.event.get():
                if event.type == pygame.MOUSEMOTION:
                    mouse_pos = pygame.mouse.get_pos()
                    square.set_pos(*mouse_pos)
                if event.type == QUIT:
                    running = False

            while game_over:
                pygame.mouse.set_visible(True)
                # Text
                draw_text('{}  points'.format(points), default_font, screen,
                          WIDTH / 2, 20, GREEN)
                # Transparent surface
                transp_surf = pygame.Surface((WIDTH, HEIGHT))
                transp_surf.set_alpha(200)
                screen.blit(transp_surf, transp_surf.get_rect())

                draw_text('You lose', pygame.font.Font(None, 40), screen,
                          WIDTH / 2, HEIGHT / 3, RED)
                draw_text('To play again press C or any mouse button',
                          default_font, screen, WIDTH / 2, HEIGHT / 2.1, GREEN)
                draw_text('To quit the game press Q', default_font, screen,
                          WIDTH / 2, HEIGHT / 1.9, GREEN)
                draw_text('Press F11 to toggle full screen', default_font, screen,
                          WIDTH / 2, HEIGHT / 1.1, GREEN, BLACK)

                pygame.display.update()

                for event in pygame.event.get():
                    if event.type == pygame.KEYDOWN:
                        if event.key == K_F11:
                            toggle_fullscreen()
                        if event.key == pygame.K_q:
                            game_over = False
                            running = False
                        elif event.key == pygame.K_c:
                            game_over = False
                            main_loop()
                            return  # Avoids recursion
                    if event.type == pygame.MOUSEBUTTONDOWN:
                        game_over = False
                        main_loop()
                        return
                    if event.type == QUIT:
                        game_over = False
                        running = False

    start_screen()
    pygame.quit()

### Hemos terminado

Si has logrado hacer funcionar el juego, ¡enhorabuena! Si te ha quedado
alguna duda, hay algo que no entiendas o crees que he cometido un error,
por favor, deja un comentario. El código fuente completo de este juego
lo he incluido en un repositorio llamado `pygame_stuff`. El repositorio
también contiene otros juegos y plantillas de código para facilitar la
tarea de programar usando pygame. Con la dirección
<https://notabug.org/jorgesumle/pygame_stuff> puedes acceder al código y
clonar el repositorio
(`git clone https://notabug.org/jorgesumle/pygame_stuff`).
